# Copyright (C) 2002-2011 The Written Word, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id: sbdb.py 755 2011-02-08 18:17:01Z china $

import base64
import cStringIO
import lockfile
import os
import re
import select
import signal
import stat
import string
import sys
import time

from copy import copy, deepcopy
from types import IntType, SliceType, StringType, UnicodeType

import misclib

from myio import maxpos
from version import Version
from xmlutil import XMLDTDError, XMLParseError, XMLSyntaxError
from xmlutil import DTD, dtd_search, etree, pprint, objectify

# constants
prefix = '%prefix%'
datadir = prefix + '/share'
libdir = '%libdir%'
auxdir = libdir + '/aux'

# exceptions
class SbdbError (Exception):
  """base class for all exceptions raised by this module"""

  def __init__ (self, msg = None):
    self.msg = msg

  def __str__ (self):
    return self.msg

class SbdbFileNotFoundError (SbdbError):
  """file not found"""

class SbdbDBNotFoundError (SbdbError):
  """database directory not found"""

class SbdbIOError (SbdbError):
  """IO error operating on database"""

def normalize_whitespace (text):
  "Remove redundant whitespace from a string"
  return (string.join (string.split (text), ' '))

def sigchld (signum, frame):
  os.waitpid (0, os.WNOHANG)

# read sb database entries
def read_sbdb_entries (db_dir, dtd_dirs, callback = None):
  if callback:
    callback ('begin', 'reading database entries in ' + db_dir, -2)

  sb_db_dir = os.path.join (db_dir, '.sb')
  sbdb_entries = SbDBEntries (sb_db_dir)

  if not os.path.isdir (sb_db_dir):
    if callback:
      callback ('end', 'done', -2)
    return sbdb_entries

  sbdb_files = os.listdir (sb_db_dir)
  for sbdb_file in sbdb_files:
    sbdb_path = os.path.join (sb_db_dir, sbdb_file)
    if not os.path.isfile (sbdb_path):
      continue

    # skip lock files
    if sbdb_file.endswith ('.lck') or sbdb_file.endswith ('.tmp'):
      continue

    try:
      fh = open (sbdb_path, 'r')
    except (IOError, OSError), msg:
      if callback:
        callback ('end', 'failed', -2)
      raise SbdbFileNotFoundError ('the database entry for ' + sbdb_path +
                                   ' was found but the following error ' +
                                   'was encountered while trying to read ' +
                                   'it: ' + str (msg))

    # obtain read lock
    try:
      lockfile.read_lock (fh)
    except lockfile.LockfileFileLockedError, msg:
      if callback:
        callback ('end', 'failed', -2)
        callback ('warning', 'failed to obtain a read lock on ' + sbdb_path +
                  '. another process is currently updating this file. will ' +
                  'wait up to 10s for the update to be complete.', -2)
        callback ('msg', 'waiting for read lock ', -2)
      for second in range (10):
        time.sleep (1)
        if callback:
          callback ('msg', '.', -2)
        try:
          lockfile.read_lock (fh)
        except lockfile.LockfileFileLockedError:
          continue
        except lockfile.LockfileError, msg:
          if callback:
            callback ('end', ' failed', -2)
          fh.close ()
          raise SbdbError ('while waiting to obtain a read lock on ' +
                           sbdb_path +
                           ', the following error was encountered: ' +
                           str (msg))

        if callback:
          callback ('end', ' done', -2)
        break
      else:
        if callback:
          callback ('end', ' failed', -2)
        fh.close ()
        raise SbdbError ('unable to obtain read lock for ' + sbdb_path +
                         '. waited 10s for another process to finish ' +
                         'updating this file but it was not enough ' +
                         'time. maybe the other process is hung?')

    except lockfile.LockfileError, msg:
      if callback:
        callback ('end', 'failed', -2)
      fh.close ()
      raise SbdbError ('while trying to obtain a read lock on ' +
                       sbdb_path +
                       ', the following error was encountered: ' +
                       str (msg))

    try:
      xml = objectify.parse (fh)
    except XMLSyntaxError, msg:
      if callback:
        callback ('end', ' failed', -2)
      fh.close ()
      raise XMLParseError ('syntax error while parsing ' + sbdb_path +
                           ', the following error was encountered: ' +
                           str (msg))

    fh.close ()

    # Find a DTD, and use it to validate the .sb entry
    dtd = dtd_search (dtd_dirs, sbdb_path, 'sb-db.dtd')
    if dtd:
      sbdtd = DTD (dtd)
      if not sbdtd.validate (xml):
        raise XMLDTDError ('DTD validation error while parsing ' +
                           os.path.join (sb_db_dir, sbdb_file) +
                           ', the following error was encountered: ' +
                           sbdtd.error_log().filter_from_errors()[0].message)

    sbdb_entries.append (SbDBEntry (xml))

  # 'default' module always created. remove if created unnecessarily.
  for sbdb_entry in sbdb_entries:
    if not sbdb_entry.modules['default'].state:
      del sbdb_entry.modules['default']

  if callback:
    callback ('end', 'done', -2)

  return sbdb_entries

def add_sbdb_entry (sbdb_entries, type, callback, program):
  # create database directory if it does not exist
  if not os.path.isdir (sbdb_entries.db_dir):
    callback ('begin', 'creating database directory ' +
              sbdb_entries.db_dir)
    if not program.dry_run:
      try:
        os.mkdir (sbdb_entries.db_dir)
      except (IOError, OSError), msg:
        callback ('end', 'failed')
        raise SbdbIOError ('unable to create database directory ' +
                           sbdb_entries.db_dir + '. error received by \
mkdir was: ' + str (msg) + '.')
    callback ('end', 'done')

  sbdb_file = os.path.join (sbdb_entries.db_dir,
                            program.prog_name + '-' + program.prog_ver)
  tmp_sbdb_file = sbdb_file + '.' + str (os.getpid ()) + '.tmp'

  # if module installs to the same directory as another module and
  # the program names are the same, rename old database entry to
  # new sbdb_file
  if program.rename_matching_install_prefix_entries and type == 'install':
    try:
      entries = sbdb_entries[program.prog_name]
    except IndexError:
      entries = []

    for sbdb_entry in entries:
      if sbdb_entry.prog_ver == program.prog_ver:
        continue

      for module in sbdb_entry.modules.keys ():
        if module == program.module.name:
          sbdb_entry_prog = copy (program)
          sbdb_entry_prog.module = sbdb_entry.modules[module]
          sbdb_entry_prog.prog_ver = sbdb_entry.prog_ver
          if sbdb_entry_prog.module.install_base:
            sbdb_entry_prog.install_base = sbdb_entry_prog.module.install_base
          else:
            sbdb_entry_prog.install_base = program.install_base
          sbdb_entry_prog.install_name = sbdb_entry_prog.module.install_name

          if program.get_install_prefix () == \
          sbdb_entry_prog.get_install_prefix ():
            if program.module.name == 'default':
              # rename entry if no database entry for new version
              try:
                prog_entry = sbdb_entries[program.prog_name + '-' +
                                          program.prog_ver]
                callback ('note',
                          'an entry exists in the installation database ' +
                          'for ' + sbdb_entry.prog_name + '-' +
                          sbdb_entry.prog_ver + ' and ' + program.prog_name +
                          '-' + program.prog_ver + '. therefore, the ' +
                          'modules from the ' + sbdb_entry.prog_name + '-' +
                          sbdb_entry.prog_ver + ' entry will be moved to the ' +
                                  program.prog_name + '-' + program.prog_ver +
                          ' entry and the ' + sbdb_entry.prog_name + '-' +
                          sbdb_entry.prog_ver + ' entry will be removed.', -2)

                if not program.dry_run:
                  os.remove (os.path.join (sbdb_entries.db_dir,
                                           sbdb_entry.prog_name + '-' +
                                           sbdb_entry.prog_ver))

                # add modules from previous version
                for module in sbdb_entry.modules.keys ():
                  prog_entry.modules[module] = sbdb_entry.modules[module]

                del sbdb_entries[sbdb_entry.prog_name + '-' +
                                 sbdb_entry.prog_ver]
                break
              except IndexError:
                callback ('note', 'the ' + sbdb_entry.prog_name +
                                  '-' + sbdb_entry.prog_ver + ' entry in the \
installation database installs to the same directory as this module. \
therefore, the ' + sbdb_entry.prog_name + '-' + sbdb_entry.prog_ver + 
                          ' entry will be renamed to ' + program.prog_name +
                          '-' + program.prog_ver + '.', -2)

                if not program.dry_run:
                  os.rename (os.path.join (sbdb_entries.db_dir,
                                           sbdb_entry.prog_name + '-' +
                                           sbdb_entry.prog_ver),
                             os.path.join (sbdb_entries.db_dir,
                                           program.prog_name + '-' +
                                           program.prog_ver))
                new_sbdb_entry = deepcopy (sbdb_entry)
                new_sbdb_entry.prog_ver = program.prog_ver
                sbdb_entries.append (new_sbdb_entry)
                del sbdb_entries[sbdb_entry.prog_name + '-' +
                                 sbdb_entry.prog_ver]
                break
            else:
              callback ('note', 'the "' + program.module.name +
                                '" module of the ' + sbdb_entry.prog_name +
                                '-' + sbdb_entry.prog_ver + ' entry in the \
installation database installs to the same directory as this module. \
therefore, the "' + program.module.name + '" module of the ' +
                        sbdb_entry.prog_name + '-' + sbdb_entry.prog_ver + 
                                ' entry will be removed and a new entry \
for the module will be created in the ' + program.prog_name + '-' +
                        program.prog_ver + ' entry.', -2)

              remove_sbdb_install_entry (sbdb_entries, callback,
                                         sbdb_entry_prog)

  if os.path.exists (sbdb_file):
    callback ('begin', 'updating database entry with "' +
              program.module.name + '" module')
  else:
    callback ('begin', 'adding database entry for "' +
              program.module.name + '" module')

  if not program.dry_run:
    try:
      lock_fh = os.open (sbdb_file + '.lck',
                         os.O_WRONLY | os.O_EXCL | os.O_CREAT, 0660)
      os.close (lock_fh)
    except OSError, msg:
      callback ('end', 'failed')
      raise SbdbError ('unable to obtain exclusive lock on database \
entry for ' + sbdb_file + '. another process is currently updating \
this file. please investigate. if no process is updating the database \
file, then please remove the lock file ' + sbdb_file + '.lck.')

    # open existing database file in append mode to obtain lock so
    # readers will wait until update finished
    sbdb_fh = open (sbdb_file, 'a')
    lockfile.write_lock (sbdb_fh)

  modules = []
  try:
    sbdb_entry = sbdb_entries[program.prog_name + '-' + program.prog_ver]
    for module in sbdb_entry.modules.keys ():
      if module != program.module.name:
        modules.append (sbdb_entry.modules[module])
    else:
      sbdb_entry.modules[program.module.name] = program.module
  except IndexError:
    sbdb_entry = SbDBEntry ()
    sbdb_entry.prog_name = program.prog_name
    sbdb_entry.prog_ver = program.prog_ver
    sbdb_entry.prog_rev = program.sbdb.prog_rev
    sbdb_entry.prog_systype = program.prog_systype
    sbdb_entry.modules[program.module.name] = program.module

    sbdb_entries.append (sbdb_entry)

  # program.module holds the module instance for the to-be-added
  # module, which needs to be listed in addition to the existing
  # modules when rewriting the sb file below.
  sbdb_xml = SbdbXML (program, [program.module] + modules)

  if not program.dry_run:
    sbdb_xml.write (tmp_sbdb_file)

    # preserve permission of original file
    mode = os.lstat (sbdb_file)

    os.chown (tmp_sbdb_file, mode[stat.ST_UID], mode[stat.ST_GID])
    os.chmod (tmp_sbdb_file, mode[stat.ST_MODE])
    sbdb_fh.close ()
    os.rename (tmp_sbdb_file, sbdb_file)
    os.remove (sbdb_file + '.lck')

  callback ('end', 'done')

def remove_sbdb_build_entry (sbdb_entries, callback, program):
  sbdb_file = os.path.join (sbdb_entries.db_dir,
                            program.prog_name + '-' + program.prog_ver)
  tmp_sbdb_file = sbdb_file + '.' + str (os.getpid ()) + '.tmp'

  if not os.path.exists (sbdb_file):
    return

  if not program.dry_run:
    try:
      lock_fh = os.open (sbdb_file + '.lck',
                         os.O_WRONLY | os.O_EXCL | os.O_CREAT, 0660)
      os.close (lock_fh)
    except OSError, msg:
      raise SbdbError ('unable to obtain exclusive lock on database \
entry for ' + sbdb_file + '. another process is currently updating \
this file. please investigate. if no process is updating the database \
file, then please remove the lock file ' + sbdb_file + '.lck.')

    # open existing database file in append mode to obtain lock so
    # readers will wait until update finished
    sbdb_fh = open (sbdb_file, 'a')
    lockfile.write_lock (sbdb_fh)

  modules = []
  try:
    sbdb_entry = sbdb_entries[program.prog_name + '-' + program.prog_ver]
    for module in sbdb_entry.modules.keys ():
      sbdb_entry_prog = copy (program)
      sbdb_entry_prog.builddir = program.builddir
      sbdb_entry_prog.module = sbdb_entry.modules[module]
      sbdb_entry_prog.prog_name = program.prog_name
      sbdb_entry_prog.prog_ver = program.prog_ver
      sbdb_entry_prog.build_prefix = sbdb_entry_prog.get_build_prefix ()

      if (module != program.module.name and \
      sbdb_entry_prog.build_prefix != program.build_prefix) or \
      (module != program.module.name and \
      sbdb_entry_prog.module.build_name is not None):
        modules.append (sbdb_entry.modules[module])
      else:
        sbdb_entry = sbdb_entries[program.prog_name + '-' + program.prog_ver]

        if sbdb_entry.modules.has_key (module):
          callback ('begin', 'removing database entry for "' +
                    module + '" module')
          del sbdb_entry.modules[module]
          callback ('end', 'done')
  except IndexError:
    pass

  if not program.dry_run:
    # remove database entry completely if no modules remain
    if len (modules) == 0:
      sbdb_fh.close ()
      os.remove (sbdb_file)
      os.remove (sbdb_file + '.lck')

      del sbdb_entries[program.prog_name + '-' + program.prog_ver]
    else:
      # the to-be-uninstalled module was removed from the module list
      # above, and now contains the list of remaining modules used to
      # rewrite the sb file:
      sbdb_xml = SbdbXML (program, modules)
      sbdb_xml.write (tmp_sbdb_file)

      # preserve permission of original file
      mode = os.lstat (sbdb_file)

      os.chown (tmp_sbdb_file, mode[stat.ST_UID], mode[stat.ST_GID])
      os.chmod (tmp_sbdb_file, mode[stat.ST_MODE])
      sbdb_fh.close ()
      os.rename (tmp_sbdb_file, sbdb_file)
      os.remove (sbdb_file + '.lck')

def remove_sbdb_install_entry (sbdb_entries, callback, program):
  sbdb_file = os.path.join (sbdb_entries.db_dir,
                            program.prog_name + '-' + program.prog_ver)
  tmp_sbdb_file = sbdb_file + '.' + str (os.getpid ()) + '.tmp'

  if not os.path.exists (sbdb_file):
    return

  if not program.dry_run:
    try:
      lock_fh = os.open (sbdb_file + '.lck',
                         os.O_WRONLY | os.O_EXCL | os.O_CREAT, 0660)
      os.close (lock_fh)
    except OSError, msg:
      raise SbdbError ('unable to obtain exclusive lock on database \
entry for ' + sbdb_file + '. another process is currently updating \
this file. please investigate. if no process is updating the database \
file, then please remove the lock file ' + sbdb_file + '.lck.')

    # open existing database file in append mode to obtain lock so
    # readers will wait until update finished
    sbdb_fh = open (sbdb_file, 'a')
    lockfile.write_lock (sbdb_fh)

  modules = []
  try:
    sbdb_entry = sbdb_entries[program.prog_name + '-' + program.prog_ver]
    for module in sbdb_entry.modules.keys ():
      if module != program.module.name:
        modules.append (sbdb_entry.modules[module])
      else:
        if callback:
          callback ('begin', 'removing database entry for "' +
                    module + '" module')
        del sbdb_entries[program.prog_name + '-' + \
                         program.prog_ver].modules[module]
        if callback:
          callback ('end', 'done')
  except IndexError:
    pass

  if not program.dry_run:
    # remove database entry completely if no modules remain
    if len (modules) == 0:
      sbdb_fh.close ()
      os.remove (sbdb_file)
      os.remove (sbdb_file + '.lck')

      del sbdb_entries[program.prog_name + '-' + program.prog_ver]
    else:
      # the to-be-uninstalled module was removed from the module list
      # above, and now contains the list of remaining modules used to
      # rewrite the sb file:
      sbdb_xml = SbdbXML (program, modules)
      sbdb_xml.write (tmp_sbdb_file)

      # preserve permission of original file
      mode = os.lstat (sbdb_file)

      os.chown (tmp_sbdb_file, mode[stat.ST_UID], mode[stat.ST_GID])
      os.chmod (tmp_sbdb_file, mode[stat.ST_MODE])
      sbdb_fh.close ()
      os.rename (tmp_sbdb_file, sbdb_file)
      os.remove (sbdb_file + '.lck')

class SbdbXML:
  def __init__ (self, program, modules):
    self.program = program
    self.xml_version = "1.0"
    self.xml_dtd = "sb-db.dtd"
    self.xml_root = etree.Element ("programs")
    self.xprogram = etree.SubElement (self.xml_root, "program",
                                      name = program.prog_name,
                                      version = program.prog_ver,
                                      revision = str (program.sbdb.prog_rev))

    sbdb = program.sbdb
    if sbdb.d_build_name:
      self.xprogram.append (self._build_name (sbdb.d_build_name))

    if sbdb.d_install_base:
      self.xprogram.append (self._install_base (sbdb.d_install_base))

    if sbdb.d_install_name:
      self.xprogram.append (self._install_name (sbdb.d_install_name))

    if sbdb.d_dependencies:
      self.xprogram.append (self._dependencies (sbdb.d_dependencies))

    # All the existing modules should be kept in either case.
    for module in modules:
      self._module (module)

  # FILE can be an string containing a path to the file to be written to,
  # or an already opened file handle.
  def write (self, file):
    if type (file) == StringType:
      close = True
      file = open (file, 'w')
    else:
      close = False

    file.write ('<?xml version ="%s"?>\n' % self.xml_version)
    file.write ('<!DOCTYPE programs SYSTEM "%s">\n' % self.xml_dtd)

    # ugly, but fully general lxml pretty printer:
    # file.write (etree.tostring (self.xml_root, pretty_print = True))

    # beautiful, but non-general TWW .sb file pretty printer:
    pprint (self.xml_root, 72, file)

    if close:
      file.close ()

  def _module (self, module):
    if module.state is None and module.name == 'default':
      xmodule = self.xprogram
    else:
      xmodule = etree.SubElement (self.xprogram, "module",
                                  name = module.name)

      if module.systype:
        if not self.program.prog_systype or \
              (self.program.prog_systype and \
               self.program.prog_systype != module.systype):
          xmodule.attrib["systype"] = module.systype

      el = etree.SubElement (xmodule, "state")
      el.text = module.state

    sbdb = self.program.sbdb
    if module.build_name and module.build_name != sbdb.d_build_name:
      xmodule.append (self._build_name (module.build_name))

    if module.install_base and module.install_base != sbdb.d_install_base:
      xmodule.append (self._install_base (module.install_base))

    if module.install_name and module.install_name != sbdb.d_install_name:
      xmodule.append (self._install_name (module.install_name))

    if module.dependencies and module.dependencies != sbdb.d_dependencies:
      xmodule.append (self._dependencies (module.dependencies))

    return xmodule

  def _build_name (self, name):
    xbuildname = etree.Element ("build-name")
    if name: xbuildname.text = name

    return xbuildname

  def _install_base (self, base):
    xinstallbase = etree.Element ("install-base")
    if base.install_prefix_only:
      xinstallbase.attrib["install-prefix-only"] = "1"
    if base.path:
      xinstallbase.text = base.path

    return xinstallbase

  def _install_name (self, name):
    xinstallname = etree.Element ("install-name")
    if name.version:
      xinstallname.attrib["version"] = name.version
    if name.append:
      xinstallname.attrib["append"] = name.append
    if name.path:
      xinstallname.text = name.path

    return xinstallname

  def _depend (self, depend):
    xdepend = etree.Element ("depend")

    # weed through attributes we won't print
    new_attributes = []
    attributes = [ ('program', depend.program), ('var', depend.var),
                   ('module', depend.module),
                   ('install-base', depend.install_base.path),
                   ('install-name', depend.install_name.path),
                   ('version', depend.install_name.version),
                   ('path', depend.path),
                   ('module', depend.module),
                   ('add-path', depend.add_path),
                   ('lib-search-path-components',
                     depend.lib_search_path_components),
                   ('state', depend.state),
                   ('append', depend.install_name.append),
                   ('systype', depend.systype),
                   ('require', depend.require),
                   ('set-var-only', depend.set_var_only),
                   ('var-substitute', depend.var_substitute),
                   ('type', depend.type) ]

    while attributes:
      attribute = attributes.pop (0)

      attribute_name = attribute[0]
      attribute_value = attribute[1]

      # ignore default module ="default" attribute
      if attribute_name == 'module' and attribute_value == 'default':
        continue

      # ignore default state ="install" attribute
      if attribute_name == 'state' and attribute_value == 'install':
        continue

      # ignore default require ="1" attribute
      if attribute_name == 'require' and attribute_value == 1:
        continue

      # ignore default var-substitute ="1" attribute
      if attribute_name == 'var-substitute' and attribute_value == 1:
        continue

      # ignore default type ="run" attribute
      if attribute_name == 'type' and attribute_value == 'run':
        continue

      # save values to be printed
      if attribute_value or attribute_name == 'require':
        new_attributes.append ((attribute_name, attribute_value))
    attributes = new_attributes

    while attributes:
      attribute = attributes.pop (0)
      xdepend.attrib[attribute[0]] = str (attribute[1])

    if depend.test:
      xdepend.text = depend.test

    return xdepend

  def _dependencies (self, dependencies):
    xdependencies = etree.Element ("dependencies")
    if not dependencies.replace:
      xdependencies.attrib["replace"] = "0"

    # user/group dependencies first
    for depend in dependencies:
      if depend.user:
        el = etree.SubElement (xdependencies, "depend")
        el.attrib["user"] = depend.user
      if depend.group:
        el = etree.SubElement (xdependencies, "depend")
        el.attrib["group"] = depend.group

    # dependencies
    for depend in dependencies:
      # ignore non-program dependencies
      if not depend.program:
        continue

      xdependencies.append (self._depend (depend))

    return xdependencies

# read XML description file
def read_sbdb_xml (sb_obj, sbdb_entries, xmlfile, dist, modules,
                   dtd_dirs, systype_path):
  # handle SIGCHLD from popen of gpp
  signal.signal (signal.SIGCHLD, sigchld)

  # prepare XML parser to read data
  parser = objectify.makeparser (remove_comments = True)

  gpp_r, my_w = os.pipe ()
  my_r, gpp_w = os.pipe ()
  gpp = os.fork ()
  if gpp == 0:
    os.close (my_r)
    os.close (my_w)
    os.dup2 (gpp_r, 0)
    os.close (gpp_r)
    os.dup2 (gpp_w, 1)
    os.close (gpp_w)

    gnu_systype = misclib.get_gnu_systype (systype_path)
    os.execv (os.path.join (auxdir, 'bin', 'gpp'),
              [os.path.join (auxdir, 'bin', 'gpp'),
               '-U', '', '', '(', ',', ')', '(', ')', '#', '',
               '-M', '\n@', '\n', ' ', ' ', '\n', '(', ')',
               '-DSB_PP_DIST=' + dist, '-DSB_PP_SYSTYPE=' + gnu_systype])

  try:
    os.close (gpp_r)
    os.close (gpp_w)
    io = select.poll ()
    fds = 2
    gpp_output = ''				# hold output from gpp
    io.register (my_r, select.POLLIN)
    io.register (my_w, select.POLLOUT)
  
    # input file
    fh = sb_obj.open (xmlfile, 'r')

    while fds:
      events = io.poll (0)

      for event in events:
        if event[0] == my_w:
          if event[1] != select.POLLHUP:
            input_line = fh.read (1024)
            if not input_line:
              io.unregister (my_w)
              os.close (my_w)
              fds -= 1
            else:
              os.write (my_w, input_line)
          else:
            io.unregister (my_w)
            os.close (my_w)
            fds -= 1
        if event[0] == my_r:
          gpp_output_line = os.read (my_r, 1024)

          # feed as many newline-terminated strings to parser as possible
          gpp_output += gpp_output_line
          newline_pos = gpp_output.find ('\n')
          parser_input = ''
          while newline_pos != -1:
            parser_input += gpp_output[0:newline_pos + 1]

            gpp_output = gpp_output[newline_pos + 1:]
            newline_pos = gpp_output.find ('\n')

          if parser_input:
            parser.feed (parser_input)

          if not gpp_output_line or event[1] == select.POLLHUP:
            io.unregister (my_r)
            os.close (my_r)
            fds -= 1

    # return to default SIGCHLD handler
    signal.signal (signal.SIGCHLD, signal.SIG_DFL)

    # send leftover data to parser
    if gpp_output:
      parser.feed (gpp_output)

    xml = parser.close ()

    # Find a DTD, and use it to validate the .sb entry
    dtd = dtd_search (dtd_dirs, fh, 'sb-db.dtd')
    if dtd:
      sbdtd = DTD (dtd)
      if not sbdtd.validate (xml):
        raise XMLDTDError ('DTD validation error while parsing ' +
                           sb_obj.sbdb_path (xmlfile) +
                           ', the following error was encountered: ' +
                           sbdtd.error_log().filter_from_errors()[0].message)
      
  except XMLSyntaxError, msg:
    raise XMLParseError ('Syntax error while parsing ' +
                         sb_obj.sbdb_path (xmlfile) +
                         ', the following error was encountered: ' +
                         str (msg))

  finally:
    fh.close ()

  sbdb = SbDBEntry (xml, xmlfile)
  sbdb_entries.append (sbdb)

  return sbdb

# list of modules available for building
def buildable_module_names (modules):
  module_names = []
  for module_name in modules.keys ():
    module = modules[module_name]
    if module.build:
      module_names.append (module_name)
  module_names.sort ()

  return module_names

class SbDBEntries:
  def __init__ (self, db_dir):
    self.entries = []

    self.db_dir = db_dir

  def __deepcopy__ (self, memo): 
    result = self.__class__ (self.db_dir)
    if len (self.entries) > 0:          
      result.entries = self.entries[:]
    return result
    
  def __delitem__ (self, key):
    if type (key) != IntType and type (key) != StringType and \
    type (key) != UnicodeType:
      raise TypeError, key

    if type (key) == IntType:
      del self.entries[key]
    else:
      index = 0

      while index < len (self.entries):
        entry = self.entries[index]
        if entry.prog_name + '-' + entry.prog_ver == key:
          del self.entries[index]
          break

        index += 1

  def __getitem__ (self, key):
    if type (key) != IntType and type (key) != StringType and \
    type (key) != UnicodeType:
      raise TypeError, key

    if type (key) == IntType:
      return self.entries[key]
    else:
      for entry in self.entries:
        if entry.prog_name + '-' + entry.prog_ver == key:
          return entry
      else:
        # return list of entries matching program name
        prog_name_entries = []
        for entry in self.entries:
          if entry.prog_name == key:
            prog_name_entries.append (entry)
        if prog_name_entries:
          return prog_name_entries

    raise IndexError

  def __len__ (self):
    return len (self.entries)

  def __iter__ (self):
    self.entry_index = 0

    return iter (self.next, None)

  def __setitem__ (self, key, value):
    index = 0

    while index < len (self.entries):
      entry = self.entries[index]
      if entry.prog_name + '-' + entry.prog_ver == key:
        self.entries[index] = value
        break

      index += 1

  def append (self, value):
    self.entries.append (value)

  def next (self):
    if len (self.entries) == 0 or \
    len (self.entries) == self.entry_index:
      return None

    self.entry_index += 1

    return self.entries[self.entry_index - 1]

  def __str__ (self):
    return self.db_dir


# This class massages an lxml.objectify tree into an sbutils style class
# hierarchy as expected over on the other side of the interface contract.
# The actual conversion is left as late as possible, so that some parts
# of this hierarchy are not built until they are queried, so that time
# isn't wasted in slavishly constructing even the parts of the hierarchy
# that aren't referenced.
# 
# To make reading it a little easier, all variables and parameters that
# hold some subtree of the parsed xml are prefixed with an 'x'.
#
# When adding a new node type, you'll probably need to add a class for
# that type (class SbDBFoo), and you will need an SbDBEntry class method
# named ofter the xml tag that it represents (def _foo (self, ...).  Most
# of the existing class methods fall into three categories:
#
#   (i) they take xparent, and must check that there is a foo tag before
#       continuing:
#             if xparent.find ('foo') is not None: ...
#  (ii) they are called after the existence of foo is known (say, from
#       inside an iterator over all children), and take an xfoo handle
#       directly;
# (iii) they are not called at startup time by __init__, but need to
#       return their results just-in-time when one of the classes from
#       the already constructed hierarchy needs to examine more of the
#       objectify tree.
#
# In all cases, the next argument to the new class method is usually the
# hook onto which the assembled SbDBFoo instance(s) can be appended.  That
# is:
#             def _foo (self, xfoo, hook):
#               foo = SbDBFoo (xfoo.get ('some-attribute'))
#               ...
#               hook.append (foo)
###
class SbDBEntry:
  def __init__ (self, xprograms = None, filename = None, srcdir = None,
                filelist = None, callback = None):
    self.xprograms = xprograms
    self.filename = filename
    self.srcdir = srcdir
    self.filelist = filelist
    self.callback = callback

    # technically, we can have several <program>s in each <programs>,
    # but that would break several assumptions in the code...
    if xprograms is not None:
      xprogram = xprograms.find ('program')
    else:
      xprogram = None

    # Used by SbDBScripts and others to iterate through the valid script
    # tags that might be found in a DTD validated sbdb.xml file:
    self.script_tags = [ 'script-header', 'configure', 'build',
                         'test', 'install', 'uninstall', 'purge' ]

    # defaults that might be overridden by <module> entries
    self.d_build_name = None
    self.d_install_base = SbDBInstallBase ()
    self.d_install_name = SbDBInstallName ()
    self.d_sources = []
    self.d_dependencies = SbDBDependencies (self, xprogram)
    self.d_validate = SbDBValidate (self, xprogram)
    self.d_proc = SbDBScripts (self, None, xprogram)

    # global procedure scripts
    self.proc = deepcopy (self.d_proc)

    # program modules notes, licenses and changelog (populated on demand)
    self.modules = SbDBModules (self)
    self.notes = SbDBNotes (self, xprogram)
    self.licenses = SbDBLicenses (self, xprogram)
    self.changelog = SbDBChangeLog (self, xprogram)

    if xprogram is not None:
      self.prog_name = xprogram.get ('name', '')
      self.prog_ver = xprogram.get ('version', '')
      self.prog_rev = xprogram.get ('revision', '1')
      self.prog_systype = xprogram.get ('systype', '')

      self.d_build_name = self._build_name (xprogram)
      self.d_install_base = self._install_base (xprogram, self.d_install_base)
      self.d_install_name = self._install_name (xprogram, self.d_install_name)
        
      self.d_sources = self._sources (xprogram)

  def _extract_file (source_path, text):
    extract_file = True

    # extract file if it matches file in filelist
    if self.filelist:
      if not source_path in self.filelist:
        extract_file = False

    if extract_file:
      payload_size = 0
      payload = cStringIO.StringIO ()

      self.callback ('begin', source_path, 2)
          
      full_path = os.path.join (self.srcdir, source_path)
      if not os.path.exists (os.path.dirname (full_path)):
        os.makedirs (os.path.dirname (full_path))
      output_file = open (path, 'w')

      payload.write (text)
      payload_size += len (text)

      # flush buffer every 77*128 lines
      if payload_size % (77*1024*128) == 0:
        output_file.write (base64.decodestring (payload.getvalue ()))
        payload.close ()
        payload = cStringIO.StringIO ()
        payload_size = 0

      # last incomplete line from payload data
      if payload_size:
        output_file.write (base64.decodestring (payload.getvalue ()))
      payload.close ()
      if output_file:
        output_file.close ()

      self.callback ('end', 'done')

  def _build_name (self, xparent):
    xbuild_name = xparent.find ('build-name')
    if xbuild_name is not None:
      return xbuild_name.text or ''
    else:
      return self.d_build_name

  def _change (self, xchange):
    change = SbDBChange ()
    change.from_version = xchange.get ('from')
    change.to_version = xchange.get ('to')
 
    for xchild in xchange.iterchildren ():
      if xchild.tag == 'para':
        change.items.items.append (self._para (xchild))
      elif xchild.tag == 'items':
        change.items.items.append (self._items (xchild))
      elif xchild.tag == 'item':
        change.items.items.append (self._item (xchild))
      elif xchild.tag == 'orderedlist':
        change.items.items.append (self._orderedlist (xchild))
      elif xchild.tag == 'screen':
        change.items.items.append (self._screen (xchild))
      elif xchild.tag == 'unorderedlist':
        change.items.items.append (self._unorderedlist (xchild))

    return change

  def _command (self, xcommand):
    return SbDBTextGroupItemCommand (xcommand.text or '')

  # This method is called repeatedly to populate an entire SbDBDependencies
  # or SbDBValidate list on demand, when the first dependency lookup is
  # attempted.
  def _depend (self, xdepend):
    depend = SbDBDependency ()
    depend.add_path = xdepend.get ('add-path')
    depend.group = xdepend.get ('group', '')
    depend.install_base = SbDBInstallBase (xdepend.get ('install-base'))
    depend.install_name = SbDBInstallName (xdepend.get ('install-name'),
                                           xdepend.get ('version'),
                                           xdepend.get ('append'))
    depend.lib_search_path_components = \
      xdepend.get ('lib-search-path-components')
    depend.module = xdepend.get ('module', 'default')
    depend.path = xdepend.get ('path', '')
    depend.program = xdepend.get ('program') or xdepend.get ('progname')
    depend.require = int (xdepend.get ('require', '1'))
    depend.set_var_only = int (xdepend.get ('set-var-only', '0'))
    depend.state = xdepend.get ('state', 'install')
    depend.systype = xdepend.get ('systype')
    depend.type = xdepend.get ('type', 'run')
    depend.user = xdepend.get ('user', '')
    depend.var = xdepend.get ('var')
    depend.var_substitute = int (xdepend.get ('var-substitute', '1'))

    if depend.path:
      depend.program = None
      if depend.var:
        depend.set_var_only = 1

    depend.test = xdepend.text

    return depend

  def _indent (self, xindent):
    indent = SbDBTextGroupList ('indent')

    for xchild in xindent.iterchildren ():
      if xchild.tag == 'indent':
        indent.items.append (self._indent (xchild))
      elif xchild.tag == 'orderedlist':
        indent.items.append (self._orderedlist (xchild))
      elif xchild.tag == 'para':
        indent.items.append (self._para (xchild))
      elif xchild.tag == 'screen':
        indent.items.append (self._screen (xchild))
      elif xchild.tag == 'unorderedlist':
        indent.items.append (self._unorderedlist (xchild))

    return indent

  def _install_base (self, xparent, install_base):
    xinstall_base = xparent.find ('install-base')
    if xinstall_base is not None:
      install_base.install_prefix_only = \
          int (xinstall_base.get ('install-prefix-only', '0'))
      install_base.path = xinstall_base.text or ''
      return install_base
    else:
      return deepcopy (self.d_install_base)

  def _install_name (self, xparent, install_name):
    xinstall_name = xparent.find ('install-name')
    if xinstall_name is not None:
      install_name.append = xinstall_name.get ('append', '')
      install_name.version = xinstall_name.get ('version')
      install_name.path = xinstall_name.text or ''
      return install_name
    else:
      return deepcopy (self.d_install_name)

  def _item (self, xitem):
    item = SbDBTextGroupItem ()

    for xchild in xitem.iterchildren ():
      if xchild.tag == 'indent':
        item.items.append (self._indent (xchild))
      elif xchild.tag == 'para':
        item.items.append (self._para (xchild))
      elif xchild.tag == 'screen':
        item.items.append (self._screen (xchild))
      elif xchild.tag == 'orderedlist':
        item.items.append (self._orderedlist (xchild))
      elif xchild.tag == 'unorderedlist':
        item.items.append (self._unorderedlist (xchild))
      
    return item

  def _items (self, xitems):
    items = SbDBTextGroupItemGroup (xitems.get ('name'))
    
    for xchild in xitems.iterchildren ():
      if xchild.tag == 'item':
        items.items.append (self._item (xchild))
      if xchild.tag == 'para':
        items.items.append (self._para (xchild))
      if xchild.tag == 'orderedlist':
        items.items.append (self._orderedlist (xchild))
      if xchild.tag == 'screen':
        items.items.append (self._screen (xchild))
      if xchild.tag == 'unorderedlist':
        items.items.append (self._unorderedlist (xchild))

    return items

  def _license (self, xlicense):
    license = SbDBLicense ()
    license.module = xlicense.get ('module')
    license.path = xlicense.get ('path')

    for xchild in xlicense.iterchildren ():
      if xchild.tag == 'indent':
        license.items.append (self._indent (xchild))
      elif xchild.tag == 'orderedlist':
        license.items.append (self._orderedlist (xchild))
      elif xchild.tag == 'para':
        license.items.append (self._para (xchild))
      elif xchild.tag == 'screen':
        license.items.append (self._screen (xchild))
      elif xchild.tag == 'unorderedlist':
        license.items.append (self._unorderedlist (xchild))

    return license

  # Called by SbDBLicenses._jit_entries ()
  def _licenses (self, xlicenses):
    for xlicense in xlicenses.iterchildren ():
      name = xlicense.get ('module')
      if name is None:
        name = 'default'

      self.licenses.append (self._license (xlicense))

  # Called by SbDBModules._jit_entries ()
  def _module (self, xmodule, name):
    module = SbDBModule (self, xmodule)

    # attributes
    module.auto_include = int (xmodule.get ('auto-include', "1"))
    module.build = int (xmodule.get ('build', "1"))
    module.systype = xmodule.get ('systype', self.prog_systype)

    # child elements
    module.state = self._state (xmodule)
    module.build_name = self._build_name (xmodule)
    module.install_base = self._install_base (xmodule, module.install_base)
    module.install_name = self._install_name (xmodule, module.install_name)

    module.sources = self._sources (xmodule)
    module.dependencies = SbDBDependencies (self, xmodule)
    module.validate = SbDBValidate (self, xmodule)

    # If this module doesn't have all of its own scripts, then fall back
    # to the global defaults
    for script in self.script_tags:
      self._script (xmodule, module.proc, script)
      if self.d_proc[script] is not None and not module.proc[script]:
        module.proc[script] = deepcopy (self.d_proc[script])
        # This prevents the obscure corner case where d_proc[script].replace
        # is 0, causing it to be invoked twice (from the freshly copied
        # module.proc[script].data, and again from the original in d_proc).
        module.proc[script].replace = 1

    # return this module to the module cache
    return module

  def _note (self, xnote):
    note = SbDBNote (xnote.get ('type'))

    for xchild in xnote.iterchildren ():
      if xchild.tag == 'indent':
        note.items.append (self._indent (xchild))
      elif xchild.tag == 'orderedlist':
        note.items.items.append (self._orderedlist (xchild))
      elif xchild.tag == 'para':
        note.items.items.append (self._para (xchild))
      elif xchild.tag == 'screen':
        note.items.items.append (self._screen (xchild))
      elif xchild.tag == 'unorderedlist':
        note.items.items.append (self._unorderedlist (xchild))

    return note

  # Called by SbDBNotes._jit_entries ()
  def _notes (self, xnotes):
    """Notes are maintained in a separate instance variable: self.notes
    unlike most other classes which are appended to their parent node."""

    name = xnotes.get ('name', '')
    self.notes.append (name)

    for xchild in xnotes.iterchildren ():
      if xchild.tag == 'change':
        self.notes[name].append (self._change (xchild))
      elif xchild.tag == 'note':
        self.notes[name].append (self._note (xchild))
      elif xchild.tag == 'platform':
        self.notes[name].append (self._platform (xchild))

  def _orderedlist (self, xorderedlist):
    start_num = xorderedlist.get ('start-num', '1')
    type = xorderedlist.get ('type', 'decimal')

    orderedlist = SbDBTextGroupList ('orderedlist', type, start_num)

    for xitem in xorderedlist.findall ('item'):
      orderedlist.items.append (self._item (xitem))
    
    return orderedlist

  def _para (self, xpara):
    para = SbDBTextGroupPara ()
    para.align = xpara.get ('align', 'left')
    para.skip = int (xpara.get ('skip', '1'))

    # First add the element's text to para
    if xpara.text is not None and len (xpara.text) > 0:
      para.append (SbDBTextGroupItemText (xpara.text))
    
    # Then, the remaining children, in order, followed by their tails
    for xchild in xpara.iterchildren ():
      if xchild.tag == 'command':
        para.append (self._command (xchild))
      elif xchild.tag == 'ulink':
        para.append (self._ulink (xchild))

      if xchild.tail is not None and len (xchild.tail) > 0:
        para.append (SbDBTextGroupItemText (xchild.tail))

    return para

  def _patch (self, xpatch):
    patch = SbDBSource ()
    patch.arg = xpatch.get ('arg')
    patch.checksum = xpatch.get ('checksum')
    patch.checksum_type = xpatch.get ('checksum-type')
    patch.destdir = xpatch.get ('destdir')
    patch.encoding = xpatch.get ('encoding')
    patch.noop = int (xpatch.get ('noop', '0'))
    patch.path = xpatch.get ('path')
    patch.size = int (xpatch.get ('size', '0'))
    patch.systype = xpatch.get ('systype')
    patch.var = xpatch.get ('var')

    if self.srcdir and xpatch.text is not None:
      self._extract_file (patch_path, xpatch.text)

    return ('patch', patch)

  def _platform (self, xplatform):
    platform = SbDBTextGroupPlatform (xplatform.get ('name'))

    for xchild in xplatform.iterchildren ():
      if xchild.tag == 'para':
        platform.items.append (self._para (xchild))
      if xchild.tag == 'screen':
        platform.items.append (self._screen (xchild))
      if xchild.tag == 'orderedlist':
        platform.items.append (self._orderedlist (xchild))
      if xchild.tag == 'unorderedlist':
        platform.items.append (self._unorderedlist (xchild))

    return platform

  def _remove_db_entry (self, xremove):
    remove_db_entry = SbDBRemoveDBEntry ()

    remove_db_entry.add_path = xremove.get ('add-path')
    remove_db_entry.install_name = \
        SbDBInstallName (xremove.get ('install-name'),
                         xremove.get ('version'),
                         xremove.get ('append', ''))
    remove_db_entry.lib_search_path_components = \
      xremove.get ('lib-search-path-components')
    remove_db_entry.module = xremove.get ('module', 'default')
    remove_db_entry.program = xremove.get ('program')
    remove_db_entry.systype = xremove.get ('systype')
    remove_db_entry.var = xremove.get ('var')
    remove_db_entry.var_substitute = int (xremove.get ('var-substitute', '1'))

    remove_db_entry.test = xremove.text

    return remove_db_entry

  def _screen (self, xscreen):
    text = ''
    if xscreen.text is not None:
      text = xscreen.text.strip ()

    return SbDBTextGroupScreen (text)

  # Since <script-header>, <configure>, <build>, <test>, <install>, <purge>
  # and <uninstall> are so similar, they are all handled by one method:
  def _script (self, xparent, scripts, key):
    xscript = xparent.find (key)

    if xscript is not None:
      scripts[key] = SbDBScript (self, xscript)

      # Rely on the DTD validation to ensure that illegal properties aren't
      # set by the time we get here.  It's faster to just try to fetch all
      # script properties for each script type, than to build some fiddly
      # logic to only lookup properties that are legal per script tag:
      scripts[key].auto_include = int (xscript.get ('auto-include', '1'))
      scripts[key].replace = int (xscript.get ('replace', '1'))
      for xremove_db_entry in xscript.findall ('remove-db-entry'):
        scripts[key].remove_db_entries.append (self._remove_db_entry (xremove_db_entry))
      
      # First add the element's text to the script
      if xscript.text is not None:
        text = xscript.text.strip ()
        if len (text) > 0:
          scripts[key].data += text

      if key == 'test':
        # Pragmatically, even though the DTD says otherwise, we'll only
        # encounter a single <dependencies> inside, so only the first is
        # handled here:
        scripts[key].dependencies = SbDBDependencies (self, xscript)

        # Be careful not to miss any data after <dependencies>
        if xscript.find ('dependencies') is not None:
          tail = xscript.dependencies.tail.strip ()
          if len (tail) > 0:
            scripts[key].data += tail

  def _source (self, xsource):
    source = SbDBSource ()
    source.checksum = xsource.get ('checksum')
    source.checksum_type = xsource.get ('checksum-type')
    source.destdir = xsource.get ('destdir')
    source.encoding = xsource.get ('encoding')
    source.noop = int (xsource.get ('noop', '0'))
    source.path = xsource.get ('path')
    source.size = int (xsource.get ('size', '0'))
    source.systype = xsource.get ('systype')
    source.var = xsource.get ('var')

    if self.srcdir and xsource.text is not None:
      self._extract_file (source.path, xsource.text)

    return ('source', source)

  def _sources (self, xparent):
    sources = []
    xsources = xparent.find ('sources')
    if xsources is not None:
      for xchild in xsources.iterchildren ():
        if xchild.tag == 'source':
          sources.append (self._source (xchild))
        elif xchild.tag == 'patch':
          sources.append (self._patch (xchild))
    else:
      sources = self.d_sources

    return sources

  def _state (self, xparent):
    xstate = xparent.find ('state')
    if xstate is not None:
      return xstate.text or ''
    else:
      return ''

  def _ulink (self, xulink):
    link_url = xulink.get ('url')
    link_value = ''

    if xulink.text is not None:
      link_value = xulink.text.replace ('\n', ' ')
  
    return SbDBTextGroupItemLink (link_url, link_value)
    
  def _unorderedlist (self, xunorderedlist):
    unorderedlist = SbDBTextGroupList ('unorderedlist')

    for xitem in xunorderedlist.findall ('item'):
      unorderedlist.items.append (self._item (xitem))
    
    return unorderedlist

  def _validate (self, xparent, validate):
    xvalidate = xparent.find ('validate')
    if xvalidate is not None:
      for xdepend in xvalidate.iterchildren ():
        validate.append (self._depend (xdepend))
    
class SbDBModules:
  def __init__ (self, sbdb_entry):
    self.sbdb = sbdb_entry
    self.entries = {}
    
    if self.sbdb.xprograms is not None:
      for xmodule in self.sbdb.xprograms.findall (".//module[@name]"):
        self.entries[xmodule.get ('name')] = None

      # There's always a default module!
      self.entries['default'] = None

  def _jit_entries (self, key):
    # No need to findall() here, since there can be only one module
    # section for each key:
    xmodule = self.sbdb.xprograms.find (".//module[@name='%s']" % key)

    if xmodule is not None:
      self.entries[key] = self.sbdb._module (xmodule, key)

    # Where there is no <module name="default"> section, create an
    # implicit default module from the global default settings. (The
    # default proc scripts are created on demand in SbDBScripts):
    if key == 'default' and self.entries[key] is None:
      self.entries['default'] = SbDBModule (self.sbdb)
      self.entries['default'].build_name = self.sbdb.d_build_name
      self.entries['default'].install_base = self.sbdb.d_install_base
      self.entries['default'].install_name = self.sbdb.d_install_name
      self.entries['default'].sources = self.sbdb.d_sources
      self.entries['default'].dependencies = self.sbdb.d_dependencies
      self.entries['default'].validate = self.sbdb.d_validate
      self.entries['default'].systype = self.sbdb.prog_systype

  def __delitem__ (self, key):
    del self.entries[key]

  def __getitem__ (self, key):
    if self.entries[key] is None:
      self._jit_entries (key)

    return self.entries[key]

  def __setitem__ (self, key, value):
    self.entries[key] = value

  def __len__ (self):
    if self.entries[key] is None:
      self._jit_entries (key)

    return len (self.entries)

  # Also define __nonzero__ since we don't need to invoke _jit_entries
  # to respond to this:
  def __nonzero__ (self):
    # There is always a default module!
    return 1

  def keys (self):
    return self.entries.keys ()

  def has_key (self, key):
    return self.entries.has_key (key)

class SbDBModule:
  def __init__ (self, sbdb_entry, xmodule = None):
    if xmodule is None:
      self.name = 'default'
    else:
      self.name = xmodule.get ('name')

    self.auto_include = 1
    self.build = 1
    self.build_name = None
    self.dependencies = SbDBDependencies (sbdb_entry, xmodule)
    self.install_base = SbDBInstallBase ()
    self.install_name = SbDBInstallName ()
    self.lib_search_path_components = None
    self.xmodule = xmodule
    self.proc = SbDBScripts (sbdb_entry, self.name, xmodule)
    self.sources = []
    self.state = None
    self.systype = None
    self.validate = SbDBValidate (sbdb_entry, xmodule)

class SbDBTextGroup:
  def __init__ (self):
    self.items = SbDBTextGroupItems ()

class SbDBNote (SbDBTextGroup):
  def __init__ (self, type):
    self.type = type
    SbDBTextGroup.__init__ (self)

class SbDBTextGroupPlatform:
  def __init__ (self, name):
    self.type = 'platform'

    self.name = name
    self.items = SbDBTextGroupItems ()

class SbDBChangeLog:
  def __init__ (self, sbdb_entry, xparent):
    self.sbdb = sbdb_entry
    self.xparent = xparent
    self.items = None

  def _jit_entries (self):
    self.items = []
    xchangelog = self.xparent.find ('changelog')
    if xchangelog is not None:
      for xchange in xchangelog.iterchildren ():
        change = SbDBChange ()
        change.date = xchange.get ('date')
        change.revision = xchange.get ('revision')
        change.author = xchange.get ('author')
        change.email = xchange.get ('email')
 
        for xchild in xchange.iterchildren ():
          if xchild.tag == 'para':
            change.items.items.append (self.sbdb._para (xchild))
          elif xchild.tag == 'items':
            change.items.append (self.sbdb._items (xchild))
          elif xchild.tag == 'item':
            change.items.items.append (self.sbdb._item (xchild))

        self.items.append (change)

  def __getitem__ (self, key):
    if self.items is None:
      self._jit_entries ()

    return self.items[key]

  def __setitem__ (self, key, value):
    self.items[key] = value

  def __len__ (self):
    if self.items is None:
      self._jit_entries ()

    return len (self.items)

  def append (self, item):
    self.items.append (item)

class SbDBChange:
  def __init__ (self):
    self.type = 'change'

    self.from_version = ''		# changes between from_version
    self.to_version = ''		# and to_version
    self.date = ''			# date of change
    self.revision = None		# revision of .sb change
    self.author = ''			# author of change
    self.email = ''			# email address of change author
    self.items = SbDBTextGroupItem ()

class SbDBLicenses:
  def __init__ (self, sbdb_entry, xparent):
    self.sbdb = sbdb_entry
    self.xparent = xparent
    self.items = None

  def _jit_entries (self):
    self.items = []
    xlicenses = self.xparent.findall ('licenses')
    for xlicenses_entry in xlicenses:
      self.sbdb._licenses (xlicenses_entry)

  def __getitem__ (self, key):
    if self.items is None:
      self._jit_entries ()

    return self.items[key]

  def __setitem__ (self, key, value):
    self.items[key] = value

  def __len__ (self):
    if self.items is None:
      self._jit_entries ()

    return len (self.items)

  def append (self, item):
    self.items.append (item)

class SbDBLicense:
  def __init__ (self, module = '', path = ''):
    self.type = 'license'

    self.module = module
    self.path = path
    self.items = SbDBTextGroupItem ()

class SbDBInstallBase:
  def __init__ (self, path = '', install_prefix_only = '0'):
    if install_prefix_only == '0':
      self.install_prefix_only = False
    else:
      self.install_prefix_only = True
    self.path = path

  def __deepcopy__ (self, memo = {}):
    result = self.__class__ (self.path, self.install_prefix_only)
    return result

  def __eq__ (self, x):
    if self.install_prefix_only == x.install_prefix_only and \
    self.path == x.path:
      return True

    return False

  def __ne__ (self, x):
    if self == x:
      return False

    return True

  def __nonzero__ (self):
    if self.path:
      return True

    return False

class SbDBInstallName:
  def __init__ (self, path = None, version = None, append = ''):
    self.append = append
    self.path = path
    self.version = version

  def __deepcopy__ (self, memo = {}):
    result = self.__class__ (self.path, self.version, self.append)
    return result

  def __eq__ (self, x):
    if self.append == x.append and \
    self.path == x.path and \
    self.version == x.version:
      return True

    return False

  def __ne__ (self, x):
    if self == x:
      return False

    return True

  def __nonzero__ (self):
    if self.append or self.path is not None or self.version:
      return True

    return False

class SbDBTextGroups:
  def __init__ (self, sbdb_entry, xparent, type, callback = None):
    self.sbdb = sbdb_entry
    self.xparent = xparent
    self.type = type
    self.callback = callback

    self.items = None

  def _jit_entries (self):
    self.items = []
    for xtype in self.xparent.findall (self.type):
      self.callback (xtype)

  def __getitem__ (self, key):
    if self.items is None and self.callback is not None:
      self._jit_entries ()

    if type (key) != IntType and type (key) != SliceType and \
    type (key) != StringType and type (key) != UnicodeType:
      raise TypeError, key

    if type (key) == IntType:
      return self.items[key]
    elif type (key) == SliceType:
      return self.items[key.start:key.stop]
    else:
      for item in self.items:
        if item.name == key:
          return item
      else:
        raise IndexError, key

  def __len__ (self):
    if self.items is None and self.callback is not None:
      self._jit_entries ()

    if self.items is None:
      return 0

    return len (self.items)

  def append (self, name = ''):
    self.items.append (SbDBTextGroupEntry (name))

class SbDBNotes (SbDBTextGroups):
  def __init__ (self, sbdb_entry, xparent):
    SbDBTextGroups.__init__ (self, sbdb_entry, xparent, 'notes', sbdb_entry._notes)

class SbDBTextGroupEntry:
  def __init__ (self, name):
    self.name = name

    self.items = []

  def __getitem__ (self, key):
    if type (key) != IntType and type (key) != SliceType:
      raise TypeError, key

    if type (key) == IntType:
      return self.items[key]
    elif type (key) == SliceType:
      return self.items[key.start:key.stop]

  def append (self, item):
    self.items.append (item)

class SbDBTextGroupItems:
  def __init__ (self):
    self.items = []

  def __getitem__ (self, key):
    if type (key) != IntType and type (key) != SliceType:
      raise TypeError, key
    if type (key) == IntType:
      if key + 1 > len (self.items):
        raise IndexError, key
      return self.items[key]
    else:
      return self.items[key.start:key.stop]

  def append (self, item):
    self.items.append (item)

class SbDBTextGroupList (SbDBTextGroupItems):
  def __init__ (self, type, list_type = None, start_num = None):
    SbDBTextGroupItems.__init__ (self)
    self.type = type
    self.list_type = list_type
    self.start_num = start_num

class SbDBTextGroupPara (SbDBTextGroupItems):
  def __init__ (self):
    SbDBTextGroupItems.__init__ (self)
    self.type = 'para'

class SbDBTextGroupItem (SbDBTextGroupItems):
  def __init__ (self):
    SbDBTextGroupItems.__init__ (self)
    self.type = 'item'
    self.group = ''				# <items> group

class SbDBTextGroupItemGroup:
  def __init__ (self, name):
    self.type = 'item group'

    self.group = name
    self.items = []

class SbDBTextGroupScreen:
  def __init__ (self, text):
    self.type = 'screen'

    self.text = text

class SbDBTextGroupItemText:
  def __init__ (self, text):
    self.type = 'text'

    self.text = text

class SbDBTextGroupItemCommand:
  def __init__ (self, text):
    self.type = 'command'

    self.text = text

class SbDBTextGroupItemLink:
  def __init__ (self, url, value):
    self.type = 'link'

    self.url = url
    self.value = value

class SbDBSource:
  def __init__ (self):
    self.arg = None
    self.build_name = None
    self.checksum = None
    self.checksum_type = None
    self.destdir = None
    self.encoding = None
    self.noop = 0
    self.path = None
    self.size = 0
    self.systype = None
    self.var = None

class SbDBValidate:
  def __init__ (self, sbdb_entry, xmodule, type = 'validate'):
    self.xmodule = xmodule
    self.sbdb = sbdb_entry
    self.type = type
    self.entries = None

  def __deepcopy__ (self, memo = {}):
    result = self.__class__ (self.sbdb, self.xmodule, self.type)
    if self.entries is not None:
      result.entries = self.entries[:]
    return result

  def __delitem__ (self, key):
    del self.entries[key]

  def _jit_entries (self):
    self.entries = []

    # Populate entries with all <depend> stanzas in this section:
    if self.xmodule is not None:
      xdependencies = self.xmodule.find (self.type)
      if xdependencies is not None:
        for xdepend in xdependencies.iterchildren ():
          self.entries.append (self.sbdb._depend (xdepend))

  # Getters need to ensure they don't run before the jit code above
  def __getitem__ (self, key):
    if self.entries is None:
      self._jit_entries ()

    return self.entries[key]

  def __iter__ (self):
    if self.entries is None:
      self._jit_entries ()

    return self.entries.__iter__ ()

  def __len__ (self):
    if self.entries is None:
      self._jit_entries ()

    return len (self.entries)

  # Setters are faster if we assume they won't be executed before the
  # caller checked we have a valid object (by running one of the
  # getter methods above).
  def __setitem__ (self, key, value):
    self.entries[key] = value

  def append (self, value):
    self.entries.append (value)

class SbDBDependencies (SbDBValidate):
  def __init__ (self, sbdb_entry, xmodule, type = 'dependencies'):
    SbDBValidate.__init__ (self, sbdb_entry, xmodule, type)
    self.replace = 1

    if xmodule is not None:
      xdependencies = xmodule.find ('dependencies')
      if xdependencies is not None:
        self.replace = int (xdependencies.get ('replace', '1'))

  def __deepcopy__ (self, memo = {}):
    result = SbDBValidate.__deepcopy__ (self, memo)
    result.replace = self.replace
    return result

class SbDBDependency:
  def __init__ (self):
    self.add_path = None
    self.group = ''
    self.install_base = SbDBInstallBase ()
    self.install_name = SbDBInstallName ()
    self.lib_search_path_components = None
    self.module = 'default'
    self.path = ''
    self.program = None
    self.require = 1
    self.set_var_only = 0
    self.state = 'install'
    self.systype = None
    self.test = ''
    self.type = ''
    self.user = ''
    self.var = None
    self.var_substitute = 1
    self.version = None

class SbDBScripts:
  def __init__ (self, sbdb_entry, name, xmodule):
    self.sbdb = sbdb_entry
    self.name = name
    self.xmodule = xmodule
    self.entries = {}

    # These will be populated on demand by __getitem__ below
    for name in self.sbdb.script_tags:
      self.entries[name] = None

  def __deepcopy__ (self, memo = {}):
    result = self.__class__ (self.sbdb, self.name, self.xmodule)
    for name in self.sbdb.script_tags:
      result.entries[name] = deepcopy (self.entries[name])
    return result

  def __delitem__ (self, key):
    del self.entries[key]

  def __getitem__ (self, key):
    if self.xmodule is not None and self.entries[key] is None:
      self.sbdb._script (self.xmodule, self.entries, key)

    if self.entries[key] is None and self.name is 'default':
      self.entries[key] = deepcopy (self.sbdb.d_proc[key])

    return self.entries[key]

  def __setitem__ (self, key, value):
    self.entries[key] = value

  def __len__ (self):
    return len (self.entries)

  def __iter__ (self):
    self.entry_names = deepcopy (self.sbdb.script_tags)
    return iter (self.next, None)

  def next (self):
    # skip empty entries (None is reserved as the iterator sentinel):
    while len (self.entry_names):
      key = self.entry_names.pop (0)
      # self.entries[key] is always None until __getitem__ populates
      # it.  self[key] calls __getitem__ implicitly:
      if self[key]:
        break
    else:
      return None

    return (key, self.entries[key])

class SbDBScript:
  def __init__ (self, sbdb_entry, xmodule):
    self.sbdb = sbdb_entry
    self.xmodule = xmodule
    self.auto_include = 1
    self.data = ''
    self.dependencies = SbDBDependencies (sbdb_entry, xmodule)
    self.remove_db_entries = []
    self.replace = 1

  def __deepcopy__ (self, memo = {}):
    result = self.__class__ (self.sbdb, self.xmodule)
    result.auto_include = self.auto_include
    result.data = self.data
    result.dependencies = self.dependencies
    result.remove_db_entries = self.remove_db_entries
    result.replace = self.replace
    return result

  def __eq__ (self, other):
    if not other:
      return False

    return self.data == other.data

  def __ne__ (self, other):
    if not other:
      return True

    return self.data != other.data

  def __nonzero__ (self):
    if len (self.data) or self.dependencies or self.remove_db_entries:
      return 1
    return 0

class SbDBRemoveDBEntry:
  def __init__ (self):
    self.add_path = None
    self.append = ''
    self.install_name = SbDBInstallName ()
    self.lib_search_path_components = None
    self.module = 'default'
    self.path = ''
    self.program = None
    self.systype = None
    self.test = ''
    self.var_substitute = None
    self.version = None
